/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package ly.count.ohos.sdk;

import ohos.app.Context;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 长字符串存储工具类
 *
 * @since 2021-04-07
 */
public class StringPreferences {
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 110, StringPreferences.class.getSimpleName());
    private static ExecutorService sExecutor = null;
    private static StringPreferences sInstance = null;
    private static String sAppDataDir = null;
    private HashMap<String, Entry> entryPool = null;

    private StringPreferences() {
        if (sExecutor == null) {
            sExecutor = Executors.newSingleThreadExecutor();
            entryPool = new HashMap<>();
        }
    }

    /**
     * 获取StringPreferences实例
     *
     * @param context App上下文
     * @return StringPreferences的实例
     */
    public static StringPreferences getInstance(Context context) {
        if (sInstance == null) {
            synchronized (StringPreferences.class) {
                if (sInstance == null) {
                    sAppDataDir = context.getApplicationContext().getDatabaseDir().getAbsolutePath();
                    sInstance = new StringPreferences();
                }
            }
        }
        return sInstance;
    }

    /**
     * 删除字符串本地数据
     *
     * @param key 键
     * @return 删除结果，true删除成功，false删除失败
     */
    public boolean delete(String key) {
        Entry entry = entryPool.get(key);
        if (entry != null) {
            return entry.delete(key);
        } else {
            return false;
        }
    }

    /**
     * 存储字符串到内存空间
     * 如果需要写入本地文件，请调用 {@link Entry#flush()}
     *
     * @param key 键
     * @param value 值
     * @return 返回一个Entry数据结构用于刷写本地文件
     */
    public Entry putString(String key, String value) {
        Entry entry = entryPool.get(key);
        if (entry == null) {
            entry = new Entry();
            entryPool.put(key, entry);
        }
        entry.putString(key, value);
        return entry;
    }

    /**
     * 从内存或者本地文件读取字符串内容
     *
     * @param key 键
     * @param defaultValue 默认值
     * @return 根据键读取的字符串，读取失败返回默认值
     */
    public String getString(String key, String defaultValue) {
        Entry entry = entryPool.get(key);
        if (entry == null) {
            entry = new Entry();
            entryPool.put(key, entry);
        }
        return entry.getString(key, defaultValue);
    }

    /**
     * 异步读取字符串内容
     *
     * @param key 键
     * @param defaultValue 默认值
     * @param callback 异步回调，根据键读取的值会在回调中返回
     */
    public void getStringAsync(String key, String defaultValue, ReadCallback callback) {
        Entry entry = entryPool.get(key);
        if (entry == null) {
            callback.onResult(defaultValue);
        } else {
            entry.getStringAsync(key, defaultValue, callback);
        }
    }

    /**
     * 长字符串存储工具类
     *
     * @since 2021-04-07
     */
    public static class Entry {
        private static final String PRE_NAME = "SP_";
        private final int valueBufferSize = 1024 * 1024 * 2;
        private File file;
        private SoftReference<String> memoryCache = null;

        private void prepareKeyFile(String key) {
            if (file == null) {
                file = new File(sAppDataDir + File.separator + PRE_NAME + key);
            }
        }

        /**
         * 将字符串内容异步写入本地文件
         */
        public void flush() {
            if (memoryCache == null || memoryCache.get() == null) {
                return;
            }
            writeString();
        }

        private void putString(String key, String value) {
            HiLog.debug(LABEL, "write into memory: " + value);
            memoryCache = new SoftReference<>(value);
            prepareKeyFile(key);
            if (!file.exists()) {
                try {
                    boolean isCreated = file.createNewFile();
                    if (!isCreated) {
                        HiLog.warn(LABEL, "Can not create file name " + key);
                    }
                } catch (IOException e) {
                    HiLog.error(LABEL, e.getMessage());
                }
            }
        }

        private String getString(String key, String defaultValue) {
            prepareKeyFile(key);
            HiLog.debug(LABEL, "read entry");
            if (memoryCache != null && memoryCache.get() != null) {
                HiLog.debug(LABEL, "read from memory: " + memoryCache.get());
                return memoryCache.get();
            } else {
                return readString(defaultValue);
            }
        }

        private void getStringAsync(String key, String defaultValue, ReadCallback callback) {
            prepareKeyFile(key);
            HiLog.debug(LABEL, "read entry");
            sExecutor.execute(() -> {
                String result = readString(defaultValue);
                callback.onResult(result);
            });
        }

        private String readString(String defaultValue) {
            FileInputStream fis = null;
            FileChannel fc = null;
            String result;
            StringBuilder sb = new StringBuilder();
            try {
                fis = new FileInputStream(file);
                fc = fis.getChannel();
                ByteBuffer buffer = ByteBuffer.allocate(valueBufferSize);
                while (true) {
                    buffer.clear();
                    int length = fc.read(buffer);
                    final int empty = -1;
                    if (length == empty) {
                        break;
                    }
                    byte[] bytes = buffer.array();
                    String str = new String(bytes, 0, length);
                    sb.append(str);
                }
                result = sb.toString();
                HiLog.debug(LABEL, "read from file: " + result);
            } catch (IOException e) {
                result = defaultValue;
                HiLog.error(LABEL, "read from file error: " + e.getMessage());
            } finally {
                try {
                    if (fc != null) {
                        fc.close();
                    }
                    if (fis != null) {
                        fis.close();
                    }
                } catch (IOException e) {
                    HiLog.error(LABEL, e.getMessage());
                }
            }
            if (memoryCache == null || memoryCache.get() == null) {
                memoryCache = new SoftReference<>(result);
            }
            return result;
        }

        private void writeString() {
            String value = memoryCache.get();
            HiLog.debug(LABEL, "write into file: " + value);
            sExecutor.execute(() -> {
                HiLog.debug(LABEL, "write files");
                FileOutputStream fos = null;
                FileChannel fc = null;
                try {
                    fos = new FileOutputStream(file, false);
                    fc = fos.getChannel();
                    byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
                    ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
                    buffer.put(bytes);
                    buffer.flip();
                    fc.write(buffer);
                    fos.flush();
                } catch (IOException e) {
                    HiLog.error(LABEL, "write to file error: " + e.getMessage());
                } finally {
                    try {
                        if (fc != null) {
                            fc.close();
                        }
                        if (fos != null) {
                            fos.close();
                        }
                    } catch (IOException e) {
                        HiLog.error(LABEL, e.getMessage());
                    }
                }
            });
        }

        private synchronized boolean delete(String key) {
            prepareKeyFile(key);
            if (file != null && file.exists()) {
                return file.delete();
            }
            return false;
        }
    }

    /**
     * 长字符串异步读取回调
     *
     * @since 2021-04-07
     */
    public interface ReadCallback {
        /**
         * 返回异步读取的字符串内容
         *
         * @param result 异步读取的字符串内容，非空
         */
        void onResult(String result);
    }
}
